package cn.bincker.mybatis.encrypt.core.impl;

import cn.bincker.mybatis.encrypt.converter.EncryptConvertRegister;
import cn.bincker.mybatis.encrypt.converter.EncryptConverter;
import cn.bincker.mybatis.encrypt.converter.utils.BinaryUtils;
import cn.bincker.mybatis.encrypt.core.*;
import cn.bincker.mybatis.encrypt.entity.EncryptProperty;
import cn.bincker.mybatis.encrypt.entity.IntegrityCheckProperty;
import cn.bincker.mybatis.encrypt.entity.SecurityProperty;
import cn.bincker.mybatis.encrypt.exception.DataIntegrityException;
import cn.bincker.mybatis.encrypt.exception.MybatisEncryptException;
import cn.bincker.mybatis.encrypt.reflection.ReflectionUtils;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

@Slf4j
public class DefaultEncryptExecutor implements EncryptExecutor, ServletRequestListener {

    private final Encryptor encryptor;
    private final EncryptConvertRegister encryptConvertRegister;
    private final Map<Class<?>, Map<String, SecurityProperty>> encryptPropertyCache;
    @Setter
    private ExecutorService executorService;
    private final EncryptKeyProvider keyProvider;
    private final ThreadLocal<StatisticInfo> statisticThreadLocal = new ThreadLocal<>();
    private final AtomicLong handleItemCount;
    private final Thread debugPointThread;
    private final EncryptCacheManager encryptCacheManager;

    public DefaultEncryptExecutor(Encryptor encryptor, EncryptConvertRegister encryptConvertRegister, EncryptKeyProvider keyProvider, EncryptCacheManager encryptCacheManager) {
        this.encryptor = encryptor;
        this.encryptConvertRegister = encryptConvertRegister;
        this.keyProvider = keyProvider;
        this.encryptCacheManager = encryptCacheManager;
        encryptPropertyCache = new ConcurrentHashMap<>();
        executorService = new ForkJoinPool(Runtime.getRuntime().availableProcessors() * 2);
        handleItemCount = new AtomicLong();
        if (log.isDebugEnabled()){
            debugPointThread = new Thread(()->{
                while (true){
                    try {
                        //noinspection BusyWait
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    var speed = handleItemCount.getAndSet(0);
                    if (speed > 0) log.debug("encrypt speed={}/s\tworks={}", speed, ((ForkJoinPool)executorService).getRunningThreadCount());
                }
            }, "encrypt-debug-log");
            debugPointThread.start();
        }else{
            debugPointThread = null;
        }
    }

    @Override
    public Encryptor getEncryptor() {
        return encryptor;
    }

    @Override
    public EncryptKeyProvider getKeyProvider() {
        return keyProvider;
    }

    @Override
    public EncryptConvertRegister getConverterRegister() {
        return encryptConvertRegister;
    }

    @Override
    public boolean isEncryptField(Class<?> clazz, String fieldName) {
        return encryptPropertyCache.computeIfAbsent(clazz, ReflectionUtils::getEncryptProperties).containsKey(fieldName);
    }

    @Override
    public Optional<SecurityProperty> getSecurityProperty(Class<?> clazz, String fieldName) {
        return Optional.ofNullable(encryptPropertyCache.computeIfAbsent(clazz, ReflectionUtils::getEncryptProperties).get(fieldName));
    }

    @Override
    public boolean hasEncryptField(Class<?> clazz) {
        return !encryptPropertyCache.computeIfAbsent(clazz, ReflectionUtils::getEncryptProperties).isEmpty();
    }

    @Override
    public Map<String, SecurityProperty> getEncryptFields(Class<?> clazz) {
        return encryptPropertyCache.computeIfAbsent(clazz, ReflectionUtils::getEncryptProperties);
    }

    @Override
    public byte[] encrypt(EncryptProperty encryptProperty, Object target, Object value) {
        if (value == null) return null;
        byte[] rawData = convertBytes(value);
        var digest = encryptor.messageDigest(rawData, keyProvider.getSalt(encryptProperty, target));
        var encryptedData = encryptor.encrypt(rawData, keyProvider.getKey(encryptProperty, target));
        var result = new byte[encryptedData.length + digest.length];
        System.arraycopy(encryptedData, 0, result, 0, encryptedData.length);
        System.arraycopy(digest, 0, result, encryptedData.length, digest.length);
        return result;
    }

    @Override
    public Future<?> putEncryptTask(EncryptProperty property, Object target, Object data) {
        return putTask(()->encrypt(property, target, data));
    }

    private byte[] convertBytes(Object value) {
        if (value == null) return new byte[0];
        byte[] rawData;
        var valueType = value.getClass();
        if (valueType != byte[].class){
            //noinspection unchecked
            var converter = (EncryptConverter<Object>) encryptConvertRegister.getConverter(valueType)
                    .orElseThrow(()->new MybatisEncryptException("not found converter: type = " + valueType));
            rawData = converter.toBinary(value);
        }else {
            rawData = (byte[]) value;
        }
        return rawData;
    }

    @Override
    public Object decrypt(EncryptProperty property, Object target, byte[] data) {
        if (data == null) return null;
        if (data.length < encryptor.getMessageDigestLength())
            throw new MybatisEncryptException(
                    "invalid encrypted data: data too short. type=" + property.getType() +
                            "\tfieldName=" + property.getName() +
                            " length = " + data.length +
                            "\tdata=" + BinaryUtils.binary2Hex(data)
            );
        var encryptedData = Arrays.copyOf(data, data.length - encryptor.getMessageDigestLength());
        var digestData = Arrays.copyOfRange(data, encryptedData.length, data.length);
        byte[] decryptedData;
        var key = keyProvider.getKey(property, target);
        try {
            decryptedData = encryptor.decrypt(encryptedData, key);
        }catch (Throwable e){
            throw new MybatisEncryptException("decrypt fail: data=" + BinaryUtils.binary2Hex(encryptedData) + "\tkey=" + key, e);
        }
        var currentDigestData = encryptor.messageDigest(decryptedData, keyProvider.getSalt(property, target));
        if (!Arrays.equals(currentDigestData, digestData))
            throw new DataIntegrityException(property.getType(), target, property.getName());
        Class<?> propertyType = property.getGetter().getReturnType();
        Object result;
        if (propertyType == byte[].class){
            result = decryptedData;
        }else{
            result = encryptConvertRegister.getConverter(propertyType)
                    .orElseThrow(()->new MybatisEncryptException("not found converter: type = " + propertyType))
                    .toObject(decryptedData);
        }
        return result;
    }

    @Override
    public Future<?> putDecryptTask(EncryptProperty property, Object target, byte[] data) {
        //noinspection unchecked
        var cache = (EncryptCache<Object>) encryptCacheManager.getCache(property);
        if (cache != null){
            var value = cache.get(property, target);
            if (value != null) return CompletableFuture.completedFuture(value);
        }
        return putTask(() -> {
            var value = decrypt(property, target, data);
            if (cache != null) cache.put(property, target, value);
            return value;
        });
    }

    private Future<?> putTask(Callable<?> callable){
        var statisticInfo = statisticThreadLocal.get();
        if (statisticInfo != null) statisticInfo.itemCount.incrementAndGet();
        return executorService.submit(()->{
            try {
                return callable.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                handleItemCount.incrementAndGet();
            }
        });
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        if (!log.isDebugEnabled()) return;
        statisticThreadLocal.set(new StatisticInfo());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        if (!log.isDebugEnabled()) return;
        var info = statisticThreadLocal.get();
        long itemCount = info.itemCount.get();
        if (itemCount < 1) return;
        var spendTime = System.currentTimeMillis() - info.time;
        statisticThreadLocal.remove();
        log.debug(
                "decrypt info: path={}\titem={}\tspendTime={}s\tspeed={}/s",
                ((HttpServletRequest) sre.getServletRequest()).getServletPath(),
                itemCount,
                spendTime / 1000,
                spendTime == 0 ? info.itemCount : ((int)(itemCount * 1.0 / spendTime * 1000))
        );
    }

    private static class StatisticInfo {
        private final long time = System.currentTimeMillis();
        private final AtomicLong itemCount = new AtomicLong();
    }

    @Override
    public byte[] messageDigest(IntegrityCheckProperty property, Object target) {
        return encryptor.messageDigest(getIntegrityCheckData(property, target), keyProvider.getSalt(property, target));
    }

    private byte[] getIntegrityCheckData(IntegrityCheckProperty property, Object target) {
        var dataArr = new byte[property.getCheckProperties().length][];
        var dataLength = 0;
        var checkProperty = property.getCheckProperties();
        for (int i = 0; i < checkProperty.length; i++) {
            Method method = checkProperty[i].getReadMethod();
            try {
                dataArr[i] = convertBytes(method.invoke(target));
                dataLength += dataArr[i].length;
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new MybatisEncryptException(e);
            }
        }
        var data = new byte[dataLength];
        var index = 0;
        for (byte[] bytes : dataArr) {
            System.arraycopy(bytes, 0, data, index, bytes.length);
            index += bytes.length;
        }
        return data;
    }

    @Override
    public byte[] checkIntegrity(IntegrityCheckProperty property, Object target, byte[] value) {
        var currentDigest = messageDigest(property, target);
        if (!Arrays.equals(value, currentDigest))
            throw new DataIntegrityException(property.getType(), target, property.getName());
        return value;
    }

    @Override
    public Future<?> putIntegrityCheckTask(IntegrityCheckProperty property, Object target, byte[] value) {
        return putTask(()->checkIntegrity(property, target, value));
    }
}
